//
// Copyright (c) 2024 ZettaScale Technology
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
//
// Contributors:
//   ZettaScale Zenoh Team, <zenoh@zettascale.tech>
//

#include "zenoh-pico/system/platform.h"
#include "zenoh-pico/utils/logging.h"

#if Z_FEATURE_LINK_SERIAL == 1 && Z_FEATURE_LINK_SERIAL_USB == 1

#include "pico/unique_id.h"
#include "tusb.h"

// ===== USB descriptors =====
// Copied from
// https://github.com/raspberrypi/pico-sdk/blob/master/src/rp2_common/pico_stdio_usb/stdio_usb_descriptors.c
//
#ifndef USBD_VID
#define USBD_VID (0x2E8A)  // Raspberry Pi
#endif

#ifndef USBD_PID
#if PICO_RP2040
#define USBD_PID (0x000a)  // Raspberry Pi Pico SDK CDC for RP2040
#else
#define USBD_PID (0x0009)  // Raspberry Pi Pico SDK CDC
#endif
#endif

#ifndef USBD_MANUFACTURER
#define USBD_MANUFACTURER "Raspberry Pi"
#endif

#ifndef USBD_PRODUCT
#define USBD_PRODUCT "Pico"
#endif

#define TUD_RPI_RESET_DESC_LEN 9
#if !PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE
#define USBD_DESC_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN)
#else
#define USBD_DESC_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_RPI_RESET_DESC_LEN)
#endif
#if !PICO_STDIO_USB_DEVICE_SELF_POWERED
#define USBD_CONFIGURATION_DESCRIPTOR_ATTRIBUTE (0)
#define USBD_MAX_POWER_MA (250)
#else
#define USBD_CONFIGURATION_DESCRIPTOR_ATTRIBUTE TUSB_DESC_CONFIG_ATT_SELF_POWERED
#define USBD_MAX_POWER_MA (1)
#endif

#define USBD_ITF_CDC (0)  // needs 2 interfaces
#if !PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE
#define USBD_ITF_MAX (2)
#else
#define USBD_ITF_RPI_RESET (2)
#define USBD_ITF_MAX (3)
#endif

#define USBD_CDC_EP_CMD (0x81)
#define USBD_CDC_EP_OUT (0x02)
#define USBD_CDC_EP_IN (0x82)
#define USBD_CDC_CMD_MAX_SIZE (8)
#define USBD_CDC_IN_OUT_MAX_SIZE (64)

#define USBD_STR_0 (0x00)
#define USBD_STR_MANUF (0x01)
#define USBD_STR_PRODUCT (0x02)
#define USBD_STR_SERIAL (0x03)
#define USBD_STR_CDC (0x04)
#define USBD_STR_RPI_RESET (0x05)

// Note: descriptors returned from callbacks must exist long enough for transfer to complete

static const tusb_desc_device_t usbd_desc_device = {
    .bLength = sizeof(tusb_desc_device_t),
    .bDescriptorType = TUSB_DESC_DEVICE,
// On Windows, if bcdUSB = 0x210 then a Microsoft OS 2.0 descriptor is required, else the device won't be detected
// This is only needed for driverless access to the reset interface - the CDC interface doesn't require these
// descriptors for driverless access, but will still not work if bcdUSB = 0x210 and no descriptor is provided. Therefore
// always use bcdUSB = 0x200 if the Microsoft OS 2.0 descriptor isn't enabled
#if PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE && PICO_STDIO_USB_RESET_INTERFACE_SUPPORT_MS_OS_20_DESCRIPTOR
    .bcdUSB = 0x0210,
#else
    .bcdUSB = 0x0200,
#endif
    .bDeviceClass = TUSB_CLASS_MISC,
    .bDeviceSubClass = MISC_SUBCLASS_COMMON,
    .bDeviceProtocol = MISC_PROTOCOL_IAD,
    .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
    .idVendor = USBD_VID,
    .idProduct = USBD_PID,
    .bcdDevice = 0x0100,
    .iManufacturer = USBD_STR_MANUF,
    .iProduct = USBD_STR_PRODUCT,
    .iSerialNumber = USBD_STR_SERIAL,
    .bNumConfigurations = 1,
};

#define TUD_RPI_RESET_DESCRIPTOR(_itfnum, _stridx)                                               \
    /* Interface */                                                                              \
    9, TUSB_DESC_INTERFACE, _itfnum, 0, 0, TUSB_CLASS_VENDOR_SPECIFIC, RESET_INTERFACE_SUBCLASS, \
        RESET_INTERFACE_PROTOCOL, _stridx,

static const uint8_t usbd_desc_cfg[USBD_DESC_LEN] = {
    TUD_CONFIG_DESCRIPTOR(1, USBD_ITF_MAX, USBD_STR_0, USBD_DESC_LEN, USBD_CONFIGURATION_DESCRIPTOR_ATTRIBUTE,
                          USBD_MAX_POWER_MA),

    TUD_CDC_DESCRIPTOR(USBD_ITF_CDC, USBD_STR_CDC, USBD_CDC_EP_CMD, USBD_CDC_CMD_MAX_SIZE, USBD_CDC_EP_OUT,
                       USBD_CDC_EP_IN, USBD_CDC_IN_OUT_MAX_SIZE),

#if PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE
    TUD_RPI_RESET_DESCRIPTOR(USBD_ITF_RPI_RESET, USBD_STR_RPI_RESET)
#endif
};

static char usbd_serial_str[PICO_UNIQUE_BOARD_ID_SIZE_BYTES * 2 + 1];

static const char *const usbd_desc_str[] = {
    [USBD_STR_MANUF] = USBD_MANUFACTURER, [USBD_STR_PRODUCT] = USBD_PRODUCT,
    [USBD_STR_SERIAL] = usbd_serial_str,  [USBD_STR_CDC] = "Board CDC",
#if PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE
    [USBD_STR_RPI_RESET] = "Reset",
#endif
};

const uint8_t *tud_descriptor_device_cb(void) { return (const uint8_t *)&usbd_desc_device; }

const uint8_t *tud_descriptor_configuration_cb(__unused uint8_t index) { return usbd_desc_cfg; }

const uint16_t *tud_descriptor_string_cb(uint8_t index, __unused uint16_t langid) {
#ifndef USBD_DESC_STR_MAX
#define USBD_DESC_STR_MAX (20)
#elif USBD_DESC_STR_MAX > 127
#error USBD_DESC_STR_MAX too high (max is 127).
#elif USBD_DESC_STR_MAX < 17
#error USBD_DESC_STR_MAX too low (min is 17).
#endif
    static uint16_t desc_str[USBD_DESC_STR_MAX];

    // Assign the SN using the unique flash id
    if (!usbd_serial_str[0]) {
        pico_get_unique_board_id_string(usbd_serial_str, sizeof(usbd_serial_str));
    }

    uint8_t len;
    if (index == 0) {
        desc_str[1] = 0x0409;  // supported language is English
        len = 1;
    } else {
        if (index >= sizeof(usbd_desc_str) / sizeof(usbd_desc_str[0])) {
            return NULL;
        }
        const char *str = usbd_desc_str[index];
        for (len = 0; len < USBD_DESC_STR_MAX - 1 && str[len]; ++len) {
            desc_str[1 + len] = str[len];
        }
    }

    // first byte is length (including header), second byte is string type
    desc_str[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * len + 2));

    return desc_str;
}

// ===== USB UART methods =====
static _z_mutex_t _z_usb_uart_mutex;
static _z_task_t _z_usb_uart_task;
static volatile bool _z_usb_uart_task_run;

static void *_z_usb_uart_task_proc(void *) {
    while (_z_usb_uart_task_run) {
        _z_mutex_lock(&_z_usb_uart_mutex);
        tud_task();
        _z_mutex_unlock(&_z_usb_uart_mutex);
        z_sleep_ms(1);
    }
    return NULL;
}

void _z_usb_uart_init() {
    tud_init(BOARD_TUD_RHPORT);
    _z_mutex_init(&_z_usb_uart_mutex);
    _z_usb_uart_task_run = true;
    _z_task_init(&_z_usb_uart_task, NULL, &_z_usb_uart_task_proc, NULL);
    _Z_DEBUG("whating for host...");
    while (!tud_cdc_connected()) {
        z_sleep_ms(100);
    }
}

void _z_usb_uart_deinit() {
    _z_usb_uart_task_run = false;
    _z_task_join(&_z_usb_uart_task);
    _z_mutex_drop(&_z_usb_uart_mutex);
    tud_deinit(BOARD_TUD_RHPORT);
}

void _z_usb_uart_write(const uint8_t *buf, int length) {
    _z_mutex_lock(&_z_usb_uart_mutex);
    for (int i = 0; i < length;) {
        int n = length - i;
        int avail = (int)tud_cdc_write_available();
        if (n > avail) {
            n = avail;
        }
        if (n != 0) {
            int n2 = (int)tud_cdc_write(buf + i, (uint32_t)n);
            tud_task();
            tud_cdc_write_flush();
            i += n2;
        } else {
            tud_task();
            tud_cdc_write_flush();
        }
    }
    _z_mutex_unlock(&_z_usb_uart_mutex);
}

uint8_t _z_usb_uart_getc() {
    uint8_t ret = 0;
    bool ready = false;
    while (true) {
        _z_mutex_lock(&_z_usb_uart_mutex);
        ready = tud_cdc_available();
        if (ready) {
            ret = tud_cdc_read_char();
        }
        _z_mutex_unlock(&_z_usb_uart_mutex);
        if (ready) {
            break;
        } else {
            z_sleep_ms(1);
        }
    }
    return ret;
}
#endif
